package com.gitee.wsl.ext.map


/**
 * Transforms map of pairs as a keys into two-dimensional map where the first elements in the pair are used as primary keys
 * and second elements as secondary keys.
 */
fun <K1, K2, V> Map<Pair<K1, K2>, V>.toTwoLevelMap(): Map<K1, Map<K2, V>> = toTwoLevelMap(LinkedHashMap()) { LinkedHashMap<K2, V>() }

/**
 * Transforms map of pairs as a keys into two-dimensional map where the first elements in the pair are used as primary keys
 * and second elements as secondary keys. [topDestination] specifies which map should be used to store the new primary keys and
 * [bottomDestination] is used to store the new secondary keys.
 */
inline fun <K1, K2, V, M2 : MutableMap<K2, V>, M1 : MutableMap<K1, M2>> Map<Pair<K1, K2>, V>.toTwoLevelMap(
    topDestination: M1,
    bottomDestination: () -> M2
): M1 {
    this.forEach { (key, value) ->
        val (key1, key2) = key
        topDestination.getOrPut(key1, bottomDestination)[key2] = value
    }
    return topDestination
}

/**
 * Transforms list of pairs, where the first element consists of pairs into two-dimensional map where the first elements
 * from the inner pair are used as primary keys and second elements as secondary keys.
 */
fun <K1, K2, V> List<Pair<Pair<K1, K2>, V>>.toTwoLevelMap(): Map<K1, Map<K2, V>> = toTwoLevelMap(LinkedHashMap()) { LinkedHashMap<K2, V>() }

/**
 * Transforms list of pairs, where the first element consists of pairs into two-dimensional map where the first elements
 * from the inner pair are used as primary keys and second elements as secondary keys.
 * [topDestination] specifies which map should be used to store the new primary keys and [bottomDestination] is used to store the new
 * secondary keys.
 */
inline fun <K1, K2, V, M2 : MutableMap<K2, V>, M1 : MutableMap<K1, M2>> List<Pair<Pair<K1, K2>, V>>.toTwoLevelMap(
    topDestination: M1,
    bottomDestination: () -> M2
): M1 {

    this.forEach { (key, value) ->
        val (key1, key2) = key
        topDestination.getOrPut(key1, bottomDestination)[key2] = value
    }
    return topDestination
}

/**
 * Works similarly as [toTwoLevelMap] but the final map has three levels.
 */
fun <K1, K2, K3, V> Map<Triple<K1, K2, K3>, V>.toThreeLevelMap(): Map<K1, Map<K2, Map<K3, V>>> =
    toThreeLevelMap(LinkedHashMap(), { LinkedHashMap<K2, MutableMap<K3, V>>() }, { LinkedHashMap() })

/**
 * Works similarly as [toTwoLevelMap] but the final map has three levels.
 * [topDestination] specifies which map should be used to store the new primary keys,
 * [middleDestination] is used to store the new secondary keys and [bottomDestination] is used to store the new tertiary keys.
 */
inline fun <K1, K2, K3, V, M3 : MutableMap<K3, V>, M2 : MutableMap<K2, M3>, M1 : MutableMap<K1, M2>>
        Map<Triple<K1, K2, K3>, V>.toThreeLevelMap(
    topDestination: M1,
    middleDestination: () -> M2,
    bottomDestination: () -> M3
): M1 {
    this.forEach { (key, value) ->
        val (key1, key2, key3) = key
        topDestination.getOrPut(key1, middleDestination).getOrPut(key2, bottomDestination)[key3] = value
    }
    return topDestination
}

/**
 * Collects all the values from the bottom level into set.
 */
fun <K1, K2, V> Map<K1, Map<K2, V>>.getSecondLevelValues(): Set<V> = this.getSecondLevelValuesTo(LinkedHashSet())

/**
 * Collects all the values from the bottom level into the given collection [M].
 */
fun <K1, K2, V, M : MutableCollection<V>> Map<K1, Map<K2, V>>.getSecondLevelValuesTo(destination: M): M {
    this.forEach { (_, bottomMap) ->
        destination.addAll(bottomMap.values)
    }

    return destination
}

/**
 * Collects all the values from the bottom level into set.
 */
fun <K1, K2, K3, V> Map<K1, Map<K2, Map<K3, V>>>.getThirdLevelValues(): Set<V> = getThirdLevelValuesTo(LinkedHashSet())


/**
 * Collects all the values from the bottom level into the given collection [M].
 */
fun <K1, K2, K3, V, M : MutableCollection<V>> Map<K1, Map<K2, Map<K3, V>>>.getThirdLevelValuesTo(destination: M): M {
    this.forEach { (_, secondLevelMap) ->
        secondLevelMap.forEach { (_, bottomMap) ->
            destination.addAll(bottomMap.values)
        }
    }
    return destination
}
